/**
 * 
 * @Project HRL Simulation
 * @Date 20.06.2011
 * @Author Aebi Pascal, Kocher Marc, Sriranjan George
 * @School BFH-TI Biel, Projekt 1
 *
 */

package gui.objects3D;

import java.awt.BorderLayout;
import java.awt.GraphicsConfiguration;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.media.j3d.Alpha;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.PositionPathInterpolator;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.swing.JPanel;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;

import main.ControlCenter;

import utils.Agenda;
import utils.AgendaEntry;
import utils.Position;
import utils.Prop;

import com.sun.j3d.utils.behaviors.keyboard.KeyNavigatorBehavior;
import com.sun.j3d.utils.behaviors.mouse.MouseBehavior;
import com.sun.j3d.utils.behaviors.mouse.MouseRotate;
import com.sun.j3d.utils.behaviors.mouse.MouseTranslate;
import com.sun.j3d.utils.behaviors.mouse.MouseZoom;
import com.sun.j3d.utils.universe.SimpleUniverse;

import elements.Buffer;
import elements.Lift;
import elements.Load;
import exceptions.ResourceException;

/**
 * The class World3D.
 */
public class World3D extends JPanel{
	
	/** The constants for the dimensions of the loads. */
	private static final float LOAD_WIDTH = Load3D.LOADWIDTH;
	private static final float LOAD_LENGTH = Load3D.LOADLENGTH;
	private static final float LOAD_HEIGHT = Load3D.LOADHEIGHT;
	/** The constants for the dimensions of the racks. */
	private final int RACK_LENGTH;
	private final int RACK_HEIGHT;
	/** The constants for the speed of the conveyor */
	private final int SIMULATION_SPEED;
	/** The duration of the 3D simulation. */
	private int duration = 0;
	private int newDuration = 0;
	private int initialDuration = 0;
	private int factor = 2;
	/** The end time of the last agenda entry which is the maximum time. */
	private int maxTime = 0;
	/** The speed of the 3D simulation. */
	private int speed = 50;
	/** The control center. */
	private ControlCenter cc;
	/** The loads. */
	private List<Load> loads = new ArrayList<Load>();
	private BranchGroup objRoot;
	private BranchGroup animationBranch;
	private BranchGroup staticBranch;
	private SimpleUniverse simpleUni;
	private List<PositionPathInterpolator> ppiList;

	
	/**
	 * Instantiates a World3D object.
	 * 
	 */
	public World3D(ControlCenter cc) {
		
		// set control center
		this.cc = cc;
		
		// get and set the different simulation parameter from the properties file
		RACK_LENGTH = Prop.getInt(Prop.RACK_LENGTH);
		RACK_HEIGHT = Prop.getInt(Prop.RACK_HEIGHT);
		SIMULATION_SPEED = Prop.getInt(Prop.SIMULATION_SPEED);

		// get the scheduled load list
		this.loads = this.cc.getScheduledLoads();
		
		// set maxTime which is the end time of the last agenda entry of all agendas
		// (needed for the calculation of the knot values for the interpolator objects)
		for(int i = 0; i < this.loads.size(); i++){
			int endTime = this.loads.get(i).getAgenda().getLastEntry().getEndTime();
			if(this.maxTime < endTime && endTime != Integer.MAX_VALUE-1){
				this.maxTime = endTime;
			}
		}
		// increase max time (for the disappearing of the loads on the exit buffer when booking out)
		this.maxTime+=1;
		// Calculate simulation speed
		int simulationFactor = 0;
		if(this.SIMULATION_SPEED < 5){
			simulationFactor = 6-this.SIMULATION_SPEED;
			this.speed *= simulationFactor;
		}
		else if(this.SIMULATION_SPEED > 5){
			simulationFactor = this.SIMULATION_SPEED-4;
			this.speed /= simulationFactor;
		}
		
		// calculate the duration the 3D simulation depending on the maxTime and the speed
		this.duration = this.maxTime * this.speed;
		this.initialDuration = this.duration;
		
		// set layout of world3d
		setLayout(new BorderLayout());

		// get display infos
		GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
		Canvas3D canvas3D = new Canvas3D(config);
		
		// create a universe
		this.simpleUni = new SimpleUniverse(canvas3D);
		
		// create the content branch of the scene graph
		this.objRoot =  new BranchGroup();
		this.objRoot.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
		this.objRoot.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE);
		this.objRoot.setCapability(TransformGroup.ALLOW_CHILDREN_READ);
		
		this.ppiList = new ArrayList<PositionPathInterpolator>();
		
		// create and add the static/animation branch to the content branch
		this.staticBranch = this.createStaticBranch();
		this.animationBranch = this.createAnimationBranch();
		this.objRoot.addChild(staticBranch);
		this.objRoot.addChild(animationBranch);
		
		// add the content branch to the universe
		this.simpleUni.addBranchGraph(this.objRoot);
		
		// add the 3D block to the gui
		this.add(BorderLayout.CENTER, canvas3D);
	}

	/**
	 * Start the animation
	 */
	public void startAnimation(){
		for (PositionPathInterpolator ppi : this.getPpiList()) {
			if(ppi.getAlpha().isPaused()) ppi.getAlpha().resume();
		}
	}
	
	/**
	 * Pause the animation
	 */
	public void pauseAnimation(){
		for (PositionPathInterpolator ppi : this.getPpiList()) {
			ppi.getAlpha().pause();
		}
	}
	
	/**
	 * Reset the animation
	 */
	public void resetAnimation(){
		this.duration = this.initialDuration;
		for (PositionPathInterpolator ppi : this.getPpiList()) {
			Alpha alpha = ppi.getAlpha();
			if(alpha == null) ppi.setAlpha(new Alpha(1, this.duration));
			alpha.pause();
			alpha.setStartTime(System.currentTimeMillis());
			alpha.setIncreasingAlphaDuration(this.duration);
			
		}
	}
	
	/**
	 * Slow down the animation
	 */
	public void slowDownAnimation(){
		this.newDuration = (int) (this.duration * this.factor);
		
		for (PositionPathInterpolator ppi : this.getPpiList()) {
			Alpha alpha = ppi.getAlpha();
			double difference = (this.newDuration-this.duration)/(1/alpha.value());
			
			alpha.pause();
			long pauseTime = alpha.getPauseTime();
			
			alpha.setIncreasingAlphaDuration((long) (this.factor * this.duration));
			alpha.resume((long) (pauseTime-difference));
		}
		this.duration = this.newDuration;
	}
	
	/**
	 * Speed up the animation
	 */
	public void speedUpAnimation(){
		this.newDuration = (int) (this.duration/this.factor);
		
		for (PositionPathInterpolator ppi : this.getPpiList()) {
			Alpha alpha = ppi.getAlpha();
			double difference = (this.newDuration-this.duration)/(1/alpha.value());
			
			alpha.pause();
			long pauseTime = alpha.getPauseTime();
			
			alpha.setIncreasingAlphaDuration((long) (this.duration/this.factor));
			alpha.resume((long) (pauseTime-difference));
		}
		this.duration = this.newDuration;
	}
	
	/**
	 * Create the static content branch of the scene graph.
	 * 
	 * @return the static branch
	 */
	public BranchGroup createStaticBranch() {
		// Create the static branch
		BranchGroup objStaticBranch = new BranchGroup();
        objStaticBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        objStaticBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        BoundingSphere bounds = new BoundingSphere();
        
		// create the 3D objects for the conveyor
        List<Position> convCorners = this.cc.getCon().getCornerPositions();
        convCorners = this.cc.convertTo3D(convCorners);
        Point3f[] conveyor = calculateConvPoints(convCorners);
        objStaticBranch.addChild(new Conveyor3D(conveyor));
        
		// create the 3D objects for the buffers
		for(int i = 0; i < this.cc.getBuffers().size(); i++){
			Buffer buffer = this.cc.getBuffers().get(i);
			Position bufferPosition = buffer.getPosition();
			bufferPosition = this.cc.convertTo3D(bufferPosition);
			Buffer3D buffer3D = new Buffer3D(buffer.getType());
			TransformGroup bufferObject = buffer3D.setPosition(bufferPosition);
			objStaticBranch.addChild(bufferObject);
		}
		
		// create the 3D objects for the racks
		List<Position> rackPositions = new ArrayList<Position>();
		for(int i = 0; i < this.cc.getRacks().size(); i++){
			rackPositions.add(i, this.cc.getRacks().get(i).getRackPosition());
			Position rackStartPosition = rackPositions.get(i);
			rackStartPosition = this.cc.convertTo3D(rackStartPosition);
			Point3f[] rack = calculateRackPoints(rackStartPosition, RACK_HEIGHT, RACK_LENGTH);
			objStaticBranch.addChild(new Rack3D(rack));
		}
		
		// define background
		Background background = new Background();
		background.setColor(0.2f, 0.3f, 0.3f);
		background.setApplicationBounds(bounds);
		objStaticBranch.addChild(background);
		
		objStaticBranch.addChild(this.navigationObject());
//		objRoot.compile();
		return objStaticBranch;
	}
	
	/**
	 * Create the animation content branch of the scene graph.
	 * 
	 * @return the animation content branch
	 */
	public BranchGroup createAnimationBranch() {
		// set maxTime which is the end time of the last agenda entry of all agendas
		// (needed for the calculation of the knot values for the interpolator objects)
		for(int i = 0; i < this.loads.size(); i++){
			if(this.maxTime < this.loads.get(i).getAgenda().getLastEntry().getStartTime()){
				this.maxTime = this.loads.get(i).getAgenda().getLastEntry().getStartTime();
			}
		}
		// calculate the duration the 3D simulation depending on the maxTime and the speed
		this.duration = this.maxTime * this.speed;
		this.initialDuration = this.duration;
		
		// Create the the animation branch
		BranchGroup objAnimationBranch = new BranchGroup();
//		objRoot.setCapability(BranchGroup.ALLOW_DETACH);
        objAnimationBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
        objAnimationBranch.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        
        objAnimationBranch.addChild(this.liftObjects());
        objAnimationBranch.addChild(this.loadObjects());
        
        return objAnimationBranch;
	}
	
	/**
	 * Create the navigation object with the configuration for mouse and keyboard
	 * 
	 * @return the navigation object
	 */
	private BranchGroup navigationObject(){
		BoundingSphere bounds = new BoundingSphere();
		BranchGroup objNavigation = new BranchGroup();
		
        Vector3f translate = new Vector3f();
        Transform3D T3D = new Transform3D();
		TransformGroup vpTrans = new TransformGroup();
		Transform3D rotTrans = new Transform3D();
		rotTrans.rotX(Math.toRadians(300));
        vpTrans = simpleUni.getViewingPlatform().getViewPlatformTransform();
        // set the initial position for the view position
        translate.setX(8.0f);
        translate.setY(30.0f);
        translate.setZ(20.0f);
        T3D.mul(rotTrans);
		T3D.setTranslation(translate);
		vpTrans.setTransform(T3D);	

		// rotate with left mouse button
		MouseRotate rotator = new MouseRotate(MouseBehavior.INVERT_INPUT);
		rotator.setTransformGroup(vpTrans);
		rotator.setSchedulingBounds(bounds);
		objNavigation.addChild(rotator);

		// zoom with middle mouse button (ALT + left mouse button if middle mouse button does not work or exist) 
		MouseZoom zoom = new MouseZoom(MouseBehavior.INVERT_INPUT);
		zoom.setTransformGroup(vpTrans);
		zoom.setSchedulingBounds(bounds);
		objNavigation.addChild(zoom);

		// move with right mouse button
		MouseTranslate translator = new MouseTranslate(MouseBehavior.INVERT_INPUT);
		translator.setTransformGroup(vpTrans);
		translator.setSchedulingBounds(bounds);
		objNavigation.addChild(translator);
		
		// navigate with keyboard
		KeyNavigatorBehavior keyNavBeh = new KeyNavigatorBehavior(vpTrans);
		keyNavBeh.setSchedulingBounds(bounds);
		objNavigation.addChild(keyNavBeh);
		
		return objNavigation; 
	}
	
	/**
	 * Create the 3D objects for the lifts
	 * 
	 * @return
	 */
	private BranchGroup liftObjects(){
		BoundingSphere bounds = new BoundingSphere();
		BranchGroup objLifts = new BranchGroup();
		List<Lift> lifts = this.cc.getLifts();
		
		// create the 3D objects for the lifts
		for(int i = 0; i < lifts.size(); i++){
			LinkedList<Point3f> positions = new LinkedList<Point3f>();
			LinkedList<Float> knots = new LinkedList<Float>();
			// if lift doesn't have a last agenda entry, set the initial position manually
			if(lifts.get(i).getAgenda().getLastEntry().getEndPos() == null){
				Position initialPosition = lifts.get(i).getPosition();
				positions.add(new Point3f(initialPosition.getX()+0.5f, initialPosition.getZ()-0.1f, initialPosition.getY()+0.5f));
				knots.add(0.0f);
			}
			// sort agenda entries by start time for adding the movements in the correct order
			Agenda agenda = lifts.get(i).getAgenda();
			List<AgendaEntry> agendaEntriesByStartTime = agenda.sortAgendaEntriesByStartTime();
			// create the different movements for every agenda entry
			for(int j = 0; j < agendaEntriesByStartTime.size(); j++){
				// start position
				Position startPosition = agendaEntriesByStartTime.get(j).getStartPosition();
				positions.add(new Point3f(startPosition.getX()+0.5f, startPosition.getZ()-0.1f, startPosition.getY()+0.5f));
				knots.add((float)agendaEntriesByStartTime.get(j).getStartTime()/this.maxTime);
				// end position
				Position endPosition = agendaEntriesByStartTime.get(j).getEndPos();
				positions.add(new Point3f(endPosition.getX()+0.5f, endPosition.getZ()-0.1f, endPosition.getY()+0.5f));
				knots.add((float)agendaEntriesByStartTime.get(j).getEndTime()/this.maxTime);
			}
			// get the last position of the movement 
			Position endPosition = null;
			if(lifts.get(i).getAgenda().getLastEntry().getEndPos() == null){
				// if lift doesn't have a last agenda entry, set the end position equal to the start position
				endPosition = lifts.get(i).getPosition();
			} else {
				// if lift does have a last agenda entry, get the end position
				endPosition = lifts.get(i).getAgenda().getLastEntry().getEndPos();
			}
			// add the last knot and position
			positions.add(new Point3f(endPosition.getX()+0.5f, endPosition.getZ()-0.1f, endPosition.getY()+0.5f));
			knots.add(1.0f);
			// create new transform group			
			TransformGroup tgLift = new TransformGroup();
			tgLift.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
			// create new alpha object with the duration calculated earlier
			Alpha alphaLift = new Alpha(1, duration);
			alphaLift.pause();
			alphaLift.setStartTime(System.currentTimeMillis());
			// convert the linked list with the knots to an array
			Float[] knotsTemp = knots.toArray(new Float[0]);
			float[] knotsArray = new float[knotsTemp.length];
			for (int j = 0; j < knotsTemp.length; j++) {
				knotsArray[j] = knotsTemp[j];
			}
			// create the position path interpolator object which creates the movement
			PositionPathInterpolator ppiLift = new PositionPathInterpolator(
					alphaLift, tgLift, new Transform3D(), knotsArray, positions.toArray(new Point3f[0]));
			ppiLift.setSchedulingBounds(bounds);
			// add a 3D lift object to the transform group
			tgLift.addChild(new Lift3D());
			// add the transform group to the root of the branch graph
			objLifts.addChild(tgLift);
			// add the position path interpolator object to the root of the branch graph
			objLifts.addChild(ppiLift);
			this.ppiList.add(ppiLift);
		}
		
		return objLifts;
	}
	
	/**
	 * Create the 3D objects for the loads
	 * 
	 * @return
	 */
	private BranchGroup loadObjects(){
		BoundingSphere bounds = new BoundingSphere();
		BranchGroup objObjects = new BranchGroup();
		
		// create the 3D objects for the loads
		for(int i = 0; i < this.loads.size(); i++){
			LinkedList<Point3f> positions = new LinkedList<Point3f>();
			LinkedList<Float> knots = new LinkedList<Float>();
			Load load = this.loads.get(i);
			Agenda agenda = this.loads.get(i).getAgenda();
			if(load.getState() == Load.STATE.EXIT){
				int startTime = load.getAgenda().getLastEntry().getEndTime();
				Position pos = new Position(-99999, 0, RACK_LENGTH/2);
				try {
					agenda.addEntry(new AgendaEntry(startTime, pos, startTime+1, pos, load));
				} catch (ResourceException e) {
					e.printStackTrace();
				}
			}
			// sort agenda entries by start time for adding the movements in the correct order
			List<AgendaEntry> agendaEntriesByStartTime = agenda.sortAgendaEntriesByStartTime();
			// get the initial position from the first agenda entry and add the knot and position
			Position initialPosition = this.loads.get(i).getAgenda().getAgendaEntries().get(0).getStartPosition();
			positions.add(new Point3f(initialPosition.getX()+0.5f, initialPosition.getZ()+0.5f, initialPosition.getY()+0.5f));
			knots.add(0.0f);
			// create the different movements for every agenda entry
			for(int j = 0; j < agendaEntriesByStartTime.size(); j++){
				// add just the start positions of each agenda entry for a flowing movement
				Position startPosition = agendaEntriesByStartTime.get(j).getStartPosition();
				positions.add(new Point3f(startPosition.getX()+0.5f, startPosition.getZ()+0.5f, startPosition.getY()+0.5f));
				knots.add((float)agendaEntriesByStartTime.get(j).getStartTime()/this.maxTime);
			}
//			add the last position manually if the time of the last agenda entry is 2147483646 (infinite) --> movement is finished
//			if(agenda.getLastEntry().getEndTime() == 2147483646){}
			// add the last knot and position
			Position lastPosition = agenda.getLastEntry().getEndPos();
			positions.add(new Point3f(lastPosition.getX()+0.5f, lastPosition.getZ()+0.5f, lastPosition.getY()+0.5f));
			knots.add(1.0f);
			// create new transform group
			TransformGroup tgLoad = new TransformGroup();
			tgLoad.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
			// create new alpha object with the duration calculated earlier
			Alpha alphaLoad = new Alpha(1, duration);
			alphaLoad.pause();
			alphaLoad.setStartTime(System.currentTimeMillis());
			// convert the linked list with the knots to an array
			Float[] knotsTemp = knots.toArray(new Float[0]);
			float[] knotsArray = new float[knotsTemp.length];
			for (int j = 0; j < knotsTemp.length; j++) {
				knotsArray[j] = knotsTemp[j];
			}
			// create the position path interpolator object which creates the movement
			PositionPathInterpolator ppiLoad = new PositionPathInterpolator(
					alphaLoad, tgLoad, new Transform3D(), knotsArray, positions.toArray(new Point3f[0]));
			ppiLoad.setSchedulingBounds(bounds);
			// add a 3D load object to the transform group
			tgLoad.addChild(new Load3D());
			// add the transform group to the root of the branch graph
			objObjects.addChild(tgLoad);
			// add the position path interpolator object to the root of the branch graph
			objObjects.addChild(ppiLoad);
			this.ppiList.add(ppiLoad);
		}

		return objObjects;
	}
	
	/**
	 * Calculate the points for drawing the 3D conveyor objects
	 * 
	 * @param convCorners list with the corner positions of the conveyor
	 * @return the calculated points for drawing the 3D conveyor objects
	 */
	private Point3f[] calculateConvPoints(List<Position> convCorners){
		Point3f[] points = new Point3f[8];
		
		float corner1X = convCorners.get(0).getX();
		float corner1Y = convCorners.get(0).getY();
		float corner1Z = convCorners.get(0).getZ();
		
		float corner2X = convCorners.get(1).getX();
		float corner2Y = convCorners.get(1).getY();
		float corner2Z = convCorners.get(1).getZ();
		
		float corner3X = convCorners.get(2).getX();
		float corner3Y = convCorners.get(2).getY();
		float corner3Z = convCorners.get(2).getZ();
		
		float corner4X = convCorners.get(3).getX();
		float corner4Y = convCorners.get(3).getY();
		float corner4Z = convCorners.get(3).getZ();
		
		// external border
		points[0] = new Point3f(corner1X, corner1Y, corner1Z);
		points[1] = new Point3f(corner2X+1, corner2Y, corner2Z);
		points[2] = new Point3f(corner3X+1, corner3Y, corner3Z+1);
		points[3] = new Point3f(corner4X, corner4Y, corner4Z+1);
		
		// internal border
		points[4] = new Point3f(corner1X+1, corner1Y, corner1Z+1);
		points[5] = new Point3f(corner2X, corner2Y, corner2Z+1);
		points[6] = new Point3f(corner3X, corner3Y, corner3Z);
		points[7] = new Point3f(corner4X+1, corner4Y, corner4Z);
		
		return points;
	}
	
	/**
	 * Calculate the points for drawing the 3D rack objects
	 * 
	 * @param startPoint start position of the rack
	 * @param stackHeight height of the rack
	 * @param stackLength length of the rack
	 * @return the calculated points for drawing the 3D rack objects
	 */
	private Point3f[] calculateRackPoints(Position startPoint, int stackHeight, int stackLength) {
		float x = startPoint.getX();
		float y = startPoint.getY();
		float z = startPoint.getZ();

		Point3f[] points = new Point3f[8];
		
		// start points
		points[0] = new Point3f(x, y, z);
		points[1] = new Point3f(x+LOAD_LENGTH, y, z); 
		// end points
		points[2] = new Point3f(x+LOAD_LENGTH, y, (stackLength*LOAD_WIDTH)+z);
		points[3] = new Point3f(x, y, (stackLength*LOAD_WIDTH)+z);
		// start points in height
		points[4] = new Point3f(x, y+(LOAD_HEIGHT*stackHeight), z);
		points[5] = new Point3f(x+LOAD_LENGTH, y+(LOAD_HEIGHT*stackHeight), z);
		// end points in height
		points[6] = new Point3f(x+LOAD_LENGTH,y+(LOAD_HEIGHT*stackHeight), (stackLength*LOAD_WIDTH)+z); 
		points[7] = new Point3f(x, y+(LOAD_HEIGHT*stackHeight), (stackLength*LOAD_WIDTH)+z);	
	
		return points;
	}
	
	public List<PositionPathInterpolator> getPpiList() {
		return ppiList;
	}

	public int getDuration() {
		return duration;
	}
}